這篇分享的是 FlatList 使用上常常會遇到的問題和解決方式。
VirtualizedLists should never be nested inside plain ScrollViews with the same orientation because it can break windowing and other functionality - use another VirtualizedList-backed container instead.
這個警告是在說同個組件中不能有多個相同方向的 VirtualizedLists 同時存在(如 FlatList 或 SectionList)
比如下面這個例子就會出現這個警告:
<ScrollView contentContainerStyle={styles.container}>
<Text>O.O</Text>
<FlatList
data={list}
renderItem={({ item }) => <Text>{item.title}</Text>}
keyExtractor={({ id }) => id.toString()}
/>
//...
</ScrollView>
ListHeaderComponent
, ListFooterComponent
<FlatList
ListHeaderComponent={
<Text>O.O</Text>
}
data={list}
renderItem={({ item }) => <Text>{item.title}</Text>}
keyExtractor={({ id }) => id.toString()}
/>
<ScrollView contentContainerStyle={styles.container}>
<Text>O.O</Text>
{list.map(({ id, title }) => (
<Text key={id}>{title}</Text>
))}
//...
</ScrollView>
<ScrollView contentContainerStyle={styles.container}>
<Text>O.O</Text>
<ScrollView horizontal={true} contentContainerStyle={{ width: '100%', height: '100%' }}>
<FlatList
data={list}
renderItem={({ item }) => <Text>{item.title}</Text>}
keyExtractor={({ id }) => id.toString()}
/>
</ScrollView>
</ScrollView>
import { LogBox } from 'react-native';
LogBox.ignoreLogs(['VirtualizedLists should never be nested'])
若要實現滾動加載資料, 需要用到 FlatList 提供的這兩個屬性:
這邊以使用 https://picsum.photos/v2/list?page=${page}&limit=${limit}
API 為例,每次滾動到底部時就會調用 fetchMoreData 函數,並調用 API 獲取下一頁的數據。
import { useState } from "react"
import { StyleSheet, View, FlatList, Text, ScrollView, Image } from "react-native"
interface ImageProps {
id: string
author: string
width: number
height: number
url: string
download_url: string
}
export const ListPage = () => {
const [data, setData] = useState<ImageProps[] | []>([])
const [page, setPage] = useState(0)
const fetchMoreData = () => {
fetch(`https://picsum.photos/v2/list?page=${page}&limit=10`)
.then(res => res.json())
.then(res => {
setData(prev => ([...prev, ...res]))
setPage(page + 1)
})
}
return (
<View style={styles.list}>
<FlatList
contentContainerStyle={{ flexGrow: 1 }}
data={data}
ItemSeparatorComponent={() => <View style={styles.divider} />}
renderItem={({ item }) => <Image source={{ uri: item.download_url }} style={{ width: 100, height: 100 }} />}
keyExtractor={({ id }) => id}
onEndReachedThreshold={0.2}
onEndReached={fetchMoreData}
/>
</View>
)
}
const styles = StyleSheet.create({
list: {
width: 300,
height: 200,
backgroundColor: 'white'
},
divider: {
height: 1,
backgroundColor: 'black'
}
})
當然這只是最基本的寫法,因為還需要考慮還有沒有剩餘的資料可以獲取,如果沒有更多的資料就不再繼續調用函數。
像上面的例子設置了 onEndReachedThreshold
為 0.2,但其實 FlatList 會在剛渲染的時候就自動觸發 onEndReached
:
const onEndReached = () => {
console.log('A')
}
return(
<FlatList
onEndReachedThreshold={0.2}
onEndReached={onEndReached}
{...}
/>
)
這是因為在初始渲染時無法得知 FlatList 準確的大小和位置,所以才會誤觸 onEndReached。StackOverflow 上針對這個bug有很多討論,比較常見的解決辦法是當 distanceFromEnd > 0
的時候再去調用 onEndReached 方法:
<FlatList
onEndReachedThreshold={0.2}
onEndReached={({ distanceFromEnd }) => {
if (distanceFromEnd > 0) {
onEndReached()
}
}}
/>
distanceFromEnd
是用戶滾動到列表底部時,列表底部距離可見區域底部的距離。
在 FlatList 初渲染且不知道大小的情況下,distanceFromEnd 可能為 0 或者非常小的值,所以將 distanceFromEnd 設為大於 0 再去調用 onEndReached 就能夠避免在初始渲染時不小心觸發。
或者也可以把 onEndReached
替換為 onMomentumScrollEnd
,在用戶停止滾動並且滾動動畫完成後才觸發 onEndReached:
<FlatList
onEndReachedThreshold={0.2}
onMomentumScrollEnd={onEndReached}
/>
這是 FlatList 的一個屬性,常用於優化。FlatList 需要事先渲染過一次,動態獲取渲染尺寸之後再真正渲染到頁面中,如果事先知道列表中的每一項高度就能使用 getItemLayout 减少一次渲染。
const ITEM_HEIGHT = 40 // 假設每一項高度固定為 40
<FlatList
// ...
getItemLayout={(_, index) => (
{
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index
}
)}
/>
注意:如果設了 getItemLayout,那麼 renderItem 的高度必須和這個高度一樣,否則加載一段列表後就會出現空白或跑版。
同樣的資料 render 耗時:
沒有使用 getItemLayout | 使用 getItemLayout |
---|---|
14.3ms | 13.7ms |
這是測試的程式碼:
import { useState } from "react"
import { StyleSheet, View, FlatList, Text, ScrollView, Image } from "react-native"
interface ImageProps {
id: string
author: string
width: number
height: number
url: string
download_url: string
}
const ITEM_HEIGHT = 100
export const ListPage = () => {
const [data, setData] = useState<ImageProps[] | []>([])
const [page, setPage] = useState(0)
const fetchMoreData = () => {
fetch(`https://picsum.photos/v2/list?page=${page}&limit=10`)
.then(res => res.json())
.then(res => {
setData(prev => ([...prev, ...res]))
setPage(page + 1)
})
}
return (
<View style={styles.list}>
<FlatList
contentContainerStyle={{ flexGrow: 1 }}
data={data}
ItemSeparatorComponent={() => <View style={styles.divider} />}
getItemLayout={(_, index) => (
{ length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index }
)}
renderItem={({ item }) => <Image source={{ uri: item.download_url }} style={{ width: 100, height: 100 }} />}
keyExtractor={({ id }) => id}
onEndReachedThreshold={0.2}
onEndReached={fetchMoreData}
/>
</View>
)
}
const styles = StyleSheet.create({
list: {
width: 300,
height: 200,
backgroundColor: 'white'
},
divider: {
height: 1,
backgroundColor: 'black'
}
})
優化這種簡單的資料其實效果甚微XD